package com.atguigu.gmall.index.service.impl;

import com.atguigu.gmall.common.bean.ResponseVo;
import com.atguigu.gmall.index.config.GmallCache;
import com.atguigu.gmall.index.feign.GmallPmsClient;
import com.atguigu.gmall.index.service.IndexService;
import com.atguigu.gmall.pms.entity.CategoryEntity;
import com.google.gson.Gson;
import io.jsonwebtoken.lang.Collections;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RLock;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Service
public class IndexServiceImpl implements IndexService {

    @Autowired
    GmallPmsClient gmallPmsClient;
    @GmallCache(cacheKeyPrefix = "cache:cates:")  //添加缓存管理的注解：暂时没有任何作用
    @Override
    public List<CategoryEntity> queryCatesAndsubs(Long c1id) {
        ResponseVo<List<CategoryEntity>> catesAndSubsResponseVo = gmallPmsClient.queryCatesAndsSubs(c1id);
        return catesAndSubsResponseVo.getData();
    }
    // 缓存key的前缀、分布式锁key的前缀、缓存的过期时间、随机时间、空值缓存的时间
//    @Override
    public List<CategoryEntity> queryCatesAndsubs2(Long c1id) {
        //1、先查询缓存是否存在 如果有直接返回
        //每个1级分类的二三级分类的结果不同，各自缓存自己的数据，key需要区分是哪个1级分类的缓存
        String key = "cache:cates:"+c1id;
        Object catesAndSubs = redisTemplate.opsForValue().get(key);
        if(catesAndSubs!=null){
            return (List<CategoryEntity>) catesAndSubs;
        }
        //锁的key必须使用c1id拼接 访问同一个1级分类的二级三级子分类的多个请求才存在竞争关系
        RLock lock = redissonClient.getLock("cache:lock:" + c1id);
        try {
            lock.lock();
            //再次判断 是否有缓存：因为有的线程在等待获取锁的期间其他线程已经获取数据并存到redis缓存中了
            catesAndSubs = redisTemplate.opsForValue().get(key);
            if(catesAndSubs!=null){
                return (List<CategoryEntity>) catesAndSubs;
            }
            //加锁：如果没有缓存，只让一个获取到锁的线程执行远程访问查询数据，其他的请求通过缓存获取数据即可
            //2、如果没有缓存，远程访问查询
            ResponseVo<List<CategoryEntity>> catesAndSubsResponseVo = gmallPmsClient.queryCatesAndsSubs(c1id);
            //3、在返回结果之前将查询到的数据存到redis中
            //为空的数据不能缓存过长的时间
            if(!Collections.isEmpty(catesAndSubsResponseVo.getData())){
                redisTemplate.opsForValue().set(key ,catesAndSubsResponseVo.getData() ,
                        7*24+ new Random().nextInt(10),TimeUnit.HOURS);
            }else{
                //查询的结果为空：为了避免缓存穿透问题，空值也缓存
                redisTemplate.opsForValue().set(key ,catesAndSubsResponseVo.getData() ,
                        20+ new Random().nextInt(10),TimeUnit.MINUTES );
            }
            return catesAndSubsResponseVo.getData();
        } finally {
            lock.unlock();
        }
    }

    //测试redis模板类的使用
    @Autowired
    StringRedisTemplate stringRedisTemplate;//只能将字符串存到redis中
    @Autowired
    RedisTemplate redisTemplate;//可以将对象类型的数据存到redis中  方法和stringRedisTemplate一样
    Gson gson = new Gson();
    @Override
    public void testRedisTemplate() {
        //向redis中缓存对象
        CategoryEntity categoryEntity = new CategoryEntity();
        categoryEntity.setName("hehe");
        categoryEntity.setId(11L);
        categoryEntity.setIcon("adsad.icon");
        //必须将复杂类型的对象转为字符串后才可以存入
        stringRedisTemplate.opsForValue().set("srtKey" , gson.toJson(categoryEntity) , 2 , TimeUnit.HOURS);
        redisTemplate.opsForValue().set("rtKey" , categoryEntity , 2, TimeUnit.HOURS);

        //获取redis中的数据
        //获取到的json需要手动转为java对象
        String srtKey = stringRedisTemplate.opsForValue().get("srtKey");
        CategoryEntity cate = gson.fromJson(srtKey, CategoryEntity.class);
        System.out.println(cate);

        CategoryEntity cate2 = (CategoryEntity)redisTemplate.opsForValue().get("rtKey");
        System.out.println("rt:  "+ cate2);

    }

    /*
    并发访问项目接口：
        执行2000次redis中的num++
        使用测试工具测试并发访问：
               ab:
               jmeter:
               postman: 持续请求测试
               swagger: 接口文档+测试

              分布式锁：
                    设置超时时间防止死锁
                    防止误删除其他线程锁
                    锁的可重入性
                    原子性操作：
                        LUA脚本使用
               Redisson:redis实现的redis集群环境的分布式锁

     */
    @Override
    public void testLock() {
        //尝试通过setnx获取分布式锁：返回成功代表获取到锁可以继续执行业务
        //返回失败，线程休眠 ，休眠后再次尝试setnx获取锁
        //获取到锁执行业务后 一定要释放锁：不释放会导致死锁
        //setIfAbsent: setnx
//        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", 1);
//        redisTemplate.expire("lock" , 5 , TimeUnit.SECONDS );
        //每个线程获取锁时都使用自己的唯一的标志 作为锁的值，删除时判断如果锁的值和自己的一样 就可以删除
        String uuid = UUID.randomUUID().toString();
        /*
            分布式锁重入性实现：
                - 判断锁如果没被使用：当前线程第一次获取锁，而且成功
                                    存入锁 指定使用次数为1，分配过期时间
                - 判断如果使用锁的是当前线程：锁重入
                                    更新锁的使用次数+1
         */
        String getLockScript = "if redis.call('exists' , KEYS[1])==0 " + //锁没被占用
                " then redis.call('hset' , KEYS[1] , ARGV[1] , 1) redis.call('expire' , KEYS[1] , 10) " +
                " return 1" +
                " elseif redis.call('hexists' , KEYS[1] , ARGV[1])==1  " + //锁已被占用，并且是自己的锁
                " then redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1) return 1" +
                " else return 0 end"; //不是自己的锁
        Boolean lock = (Boolean) redisTemplate.execute(new DefaultRedisScript(getLockScript, Boolean.class), Arrays.asList("lock"), uuid);
        //Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 5, TimeUnit.SECONDS);
        if(lock){//获取锁成功
            //获取redis中的num
            //开一个子线程：死循环-如果当前线程的lock锁 还未使用完毕 自动每隔 锁的过期时间*2/3 自动续期，当当前线程锁不存在时线程结束
            renewLock("lock",uuid , 10);
            Object num = redisTemplate.opsForValue().get("num");
            if(num==null){
                return;
            }
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
//            checkNum(uuid , "lock");//执行检查的业务：它也需要获取锁使用   锁的可重入性
            int i = Integer.parseInt(num.toString());
            i++;
            redisTemplate.opsForValue().set("num" , i);
//            int a = 1/0;
            /*
            当前线程获取分布式锁成功时：会向redis中存入 键lock，过期时间是5秒
            如果业务执行的时间超过锁的过期时间5秒，下一个线程就可以获取到锁并执行自己的业务
            当前线程执行delete删除时删除的是下一个线程设置的lock锁
             */
            //释放使用完毕的锁: 判断是否是自己的锁，如果是则释放
//            Object val = redisTemplate.opsForValue().get("lock");
//            if(val!=null && uuid.equals(val.toString())){
//                redisTemplate.delete("lock");
//            }
            /*
                删除分布式锁：
                    判断lock锁的值 是否等于当前线程的 uuid值
                        如果相等 它就是当前线程的锁，执行删除操作
                        如果不相等 返回删除失败
             */
//            String delLockScript = "if redis.call('get' , KEYS[1])== ARGV[1]" +
//                    " then return redis.call('del' , KEYS[1]) " + //成功返回1  失败返回0
//                    " else return 2 end";
            /*
            删除可重入性锁：
                如果锁不是当前线程的：
                    释放失败
                如果锁是当前线程的：
                    执行锁使用次数-1==0，删除锁
                    次数-1>0 ，释放锁失败
             */
            String delLockScript ="if redis.call('hexists', KEYS[1],ARGV[1])==0 " +
                    " then return 0 " +//锁不是当前线程的，释放失败
                    " elseif  redis.call('hincrby' , KEYS[1], ARGV[1] , -1)==0" +
                    " then redis.call('del' , KEYS[1]) return 1" + //当前线程的锁使用次数-1后为0，锁完全释放
                    " else return 2 end";
            Object execute = redisTemplate.execute(new DefaultRedisScript(delLockScript, Long.class), Arrays.asList("lock"), uuid);
            System.out.println("释放锁："+execute);
        }else{
            //获取锁失败
            try {
                //休眠
                Thread.sleep(10);
                //再次尝试获取锁
                this.testLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }


    @Autowired
    RedissonClient redissonClient;
    //使用redisson实现分布式锁
    @Override
    public void testLock2() {
        //获取锁
        RLock lock = redissonClient.getLock("lock:test2");
        try {
            lock.lock();//阻塞等待获取锁
            //获取锁成功可以继续执行业务
            String numStr = redisTemplate.opsForValue().get("num").toString();
            int num = Integer.parseInt(numStr);
            num++;
            redisTemplate.opsForValue().set("num" , num);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    @Override
    public void testCdl1() {
        RCountDownLatch cdl = redissonClient.getCountDownLatch("cdl:test");
        cdl.trySetCount(2);
        try {
            cdl.await();

            System.out.println("testCdl1停止阻塞，倒计数器的值为0");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void testCdl2() {
        RCountDownLatch cdl = redissonClient.getCountDownLatch("cdl:test");
        cdl.countDown();
        System.out.println("testCdl2执行倒计数-1的操作");
    }

    Executor executor = Executors.newCachedThreadPool();
    private void renewLock(String lock, String uuid, int i) {
        executor.execute(()->{
            int count = 1;
           String script = "if redis.call('hexists' , KEYS[1] , ARGV[1])==0  then  return 0" +
                   " else return redis.call('expire' ,KEYS[1] , ARGV[2]) end";
           while((Boolean)redisTemplate.execute(new DefaultRedisScript(script,Boolean.class) ,
                   Arrays.asList(lock) , uuid , i)){//如果当前线程的锁不存在则结束，如果存在自动续期
               //让当前线程休眠 i的2/3的时间
               System.out.println("自动续期的次数:"+ count++);
               try {
                   Thread.sleep(i*1000*2/3);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }


        });
    }

    private void checkNum(String uuid, String lock) {
        String getLockScript = "if redis.call('exists' , KEYS[1])==0 " + //锁没被占用
                " then redis.call('hset' , KEYS[1] , ARGV[1] , 1) redis.call('expire' , KEYS[1] , 100) " +
                " return 1" +
                " elseif redis.call('hexists' , KEYS[1] , ARGV[1])==1  " + //锁已被占用，并且是自己的锁
                " then redis.call('hincrby' , KEYS[1] ,ARGV[1] , 1) return 1" +
                " else return 0 end"; //不是自己的锁
        Boolean b = (Boolean) redisTemplate.execute(new DefaultRedisScript(getLockScript, Boolean.class), Arrays.asList("lock"), uuid);
        System.out.println("checkNum方法重入获取锁："+ b);

        String delLockScript ="if redis.call('hexists', KEYS[1],ARGV[1])==0 " +
                " then return 0 " +//锁不是当前线程的，释放失败
                " elseif  redis.call('hincrby' , KEYS[1], ARGV[1] , -1)==0" +
                " then redis.call('del' , KEYS[1]) return 1" + //当前线程的锁使用次数-1后为0，锁完全释放
                " else return 2 end";
        Object execute = redisTemplate.execute(new DefaultRedisScript(delLockScript, Long.class), Arrays.asList("lock"), uuid);
        System.out.println("checkNum释放锁："+execute);

    }


}
